package com.zhima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhima.constant.QuestionRedisConstant;
import com.zhima.constant.QuestionXxlJobConstant;
import com.zhima.dto.*;
import com.zhima.entity.OjQuestionAudit;
import com.zhima.entity.OjQuestionPublish;
import com.zhima.enums.QuestionAuditState;
import com.zhima.enums.QuestionPublishState;
import com.zhima.exception.ResourceNotFoundException;
import com.zhima.exception.ZhimaOjException;
import com.zhima.feignclient.JudgeRecordFeignClient;
import com.zhima.feignclient.LocalTaskFeignClient;
import com.zhima.feignclient.UserFeignClient;
import com.zhima.mapper.OjQuestionPublishMapper;
import com.zhima.service.ElasticSearchService;
import com.zhima.service.OjQuestionAuditService;
import com.zhima.service.OjQuestionPublishService;
import com.zhima.util.PageUtil;
import com.zhima.util.QuestionRedisScript;
import com.zhima.vo.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import static com.zhima.exception.ErrorCode.*;
import static com.zhima.util.AuthorizationUtil.isLoginUserResource;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author zhima
 * @since 2023-08-25
 */
@Service
public class OjQuestionPublishServiceImpl extends ServiceImpl<OjQuestionPublishMapper, OjQuestionPublish> implements OjQuestionPublishService {
    @Autowired
    private OjQuestionAuditService ojQuestionAuditService;

    @Autowired
    private LocalTaskFeignClient localTaskFeignClient;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private OjQuestionPublishMapper ojQuestionPublishMapper;

    @Autowired
    private ElasticSearchService elasticSearchService;

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private JudgeRecordFeignClient judgeRecordFeignClient;

    @Override
    public Page<QuestionPublishVO> pageQueryQuestionPublish(Long userId, Long pageNum, Long pageSize) {
        Page<OjQuestionPublish> pageQueryResult = page(new Page<>(pageNum - 1, pageSize), new LambdaQueryWrapper<OjQuestionPublish>().eq(OjQuestionPublish::getUploaderId, userId));
        return PageUtil.buildPage(pageQueryResult, pageQueryResult.getRecords().stream().map(r -> BeanUtil.copyProperties(r, QuestionPublishVO.class)).collect(Collectors.toList()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean submitPublishQuestion(Long userId, Long questionId) {
        OjQuestionAudit ojQuestionAudit = ojQuestionAuditService.lambdaQuery().eq(OjQuestionAudit::getId, questionId).one();
        if (ojQuestionAudit == null) {
            throw new ZhimaOjException(FIRST_SUBMIT_AUDIT);
        }
        // 验证改题目是否是登录用户的
        isLoginUserResource(userId, ojQuestionAudit.getUploaderId());
        // 如果状态不是审核通过，则抛出异常
        if (ojQuestionAudit.getState() != QuestionAuditState.PASS_AUDIT) {
            throw new ZhimaOjException(NOT_ALLOW_PUBLISH_NOT_PASS_AUDIT);
        }
        OjQuestionPublish ojQuestionPublish = lambdaQuery().eq(OjQuestionPublish::getId, questionId).one();
        // 第一次发布
        if (ojQuestionPublish == null) {
            ojQuestionPublish = new OjQuestionPublish();
            // 初始化这些值
            ojQuestionPublish.setSubmitNum(0);
            ojQuestionPublish.setAcNum(0);
            // 默认通过率为0
            ojQuestionPublish.setPassingRate(new BigDecimal("0"));
        }
        BeanUtil.copyProperties(ojQuestionAudit, ojQuestionPublish, "state", "advice");
        ojQuestionPublish.setState(QuestionPublishState.PUBLISHING);
        ojQuestionPublish.setAdvice(null);
        // 更新或者保存发布题目的状态、发布一个任务、删除审核表中的记录(发布成功后删除)
        String questionIdStr = String.valueOf(questionId);
        return saveOrUpdate(ojQuestionPublish)
                // 发布分三步，更新MySQL数据库、更新Redis数据库、更新ES索引库
                && localTaskFeignClient.publishTask(QuestionXxlJobConstant.PUBLISH_QUESTION, List.of(questionIdStr, questionIdStr, questionIdStr));
    }

    @Override
    public List<SimpleQuestionVO> batchQuerySimpleQuestionByIds(List<Long> questionIds) {
        return lambdaQuery()
                .in(OjQuestionPublish::getId, questionIds)
                .select(OjQuestionPublish::getId, OjQuestionPublish::getPrefix, OjQuestionPublish::getNumber, OjQuestionPublish::getTitle, OjQuestionPublish::getDifficulty)
                .list()
                .stream().map(r -> BeanUtil.copyProperties(r, SimpleQuestionVO.class))
                .collect(Collectors.toList());
    }

    @Override
    public Long queryQuestionVersion(Long questionId) {
        OjQuestionPublish ojQuestionPublish = lambdaQuery().eq(OjQuestionPublish::getId, questionId).one();
        if (ojQuestionPublish == null) {
            // 表示不存在此题目
            return -1L;
        }
        return ojQuestionPublish.getVersion();
    }

    @Override
    public Long getQuestionIdByQuestionNumber(String questionNumber) {
        return ojQuestionPublishMapper.getQuestionIdByQuestionNumber(questionNumber);
    }

    @Override
    public List<QuestionBankContestEditQuestionVO> assembleQuestion(List<Long> questionIds) {
        return ojQuestionPublishMapper.assembleQuestion(questionIds);
    }

    @Override
    public List<Integer> listFullMarks(List<Long> questionIds) {
        return ojQuestionPublishMapper.listFullMarks(questionIds);
    }

    @Override
    public FeignQuestionBasicInfoDTO queryQuestionBasicInfo(Long questionId) {
        OjQuestionPublish ojQuestionPublish = lambdaQuery().eq(OjQuestionPublish::getId, questionId).one();
        if (ojQuestionPublish == null) {
            throw new ZhimaOjException(QUESTION_NOT_PUBLISH);
        }
        return BeanUtil.copyProperties(ojQuestionPublish, FeignQuestionBasicInfoDTO.class);
    }

    @Override
    public FeignQuestionJudgeInfoDTO queryQuestionJudgeInfo(Long questionId) {
        OjQuestionPublish ojQuestionPublish = lambdaQuery().eq(OjQuestionPublish::getId, questionId).one();
        if (ojQuestionPublish == null) {
            throw new ZhimaOjException(QUESTION_NOT_PUBLISH);
        }
        return BeanUtil.copyProperties(ojQuestionPublish, FeignQuestionJudgeInfoDTO.class);
    }

    @Override
    public FeignQuestionJudgeScoreInfoDTO queryQuestionJudgeScoreInfo(Long questionId) {
        OjQuestionPublish ojQuestionPublish = lambdaQuery().eq(OjQuestionPublish::getId, questionId).one();
        if (ojQuestionPublish == null) {
            throw new ZhimaOjException(QUESTION_NOT_PUBLISH);
        }
        return BeanUtil.copyProperties(ojQuestionPublish, FeignQuestionJudgeScoreInfoDTO.class);
    }

    @Override
    public Page<QuestionTableVO> pageQueryQuestionPublishByConditions(Long pageNum, Long pageSize, Long userId, QueryQuestionCondition queryQuestionCondition) {
        Page<QuestionTableVO> ans = new Page<>(pageNum, pageSize);
        List<QuestionTableVO> questionTableVOS = elasticSearchService.searchQuestions(queryQuestionCondition, pageNum, pageSize, ans);
        ans.setRecords(questionTableVOS);
        if (!questionTableVOS.isEmpty()) {
            assembleQuestionAc(userId, questionTableVOS);
            assembleQuestionPassingRate(questionTableVOS);
        }
        return ans;
    }

    private void assembleQuestionPassingRate(List<QuestionTableVO> questionTableVOS) {
        List<Long> questionIds = questionTableVOS.stream().map(QuestionTableVO::getId).collect(Collectors.toList());
        List<BigDecimal> questionPassingRates = ojQuestionPublishMapper.listQuestionPassingRates(questionIds);
        for (int i = 0; i < questionTableVOS.size(); i++) {
            questionTableVOS.get(i).setPassingRate(questionPassingRates.get(i));
        }
    }

    /**
     * 抽取本方法主要是为了其他service调用
     *
     * @param userId
     * @param questionTableVOS
     */
    @Override
    public void assembleQuestionAc(Long userId, List<QuestionTableVO> questionTableVOS) {
        // 登录用户对此题的做题情况即是否AC
        // √方案一：题对人
        // 只取需要的数据，使用批命令一次获取多个结果
        // 方案二：人对题
        List<String> isAcArray;
        // 是登录用户查看，需要去Redis中查每道题登录用户是否AC
        if (userId != null) {
            isAcArray = CharSequenceUtil.splitTrim(
                    stringRedisTemplate.execute(
                            QuestionRedisScript.QUERY_USER_IS_AC_QUESTION_SCRIPT,
                            Arrays.asList("userId", "questionIds"),
                            String.valueOf(userId),
                            questionTableVOS.stream().map(r -> String.valueOf(r.getId())).reduce("", (a, b) -> a + b + ",")
                    ), " ");
        } else {
            isAcArray = new ArrayList<>();
            for (int i = 0; i < questionTableVOS.size(); i++) {
                isAcArray.add("0");
            }
        }
        for (int i = 0; i < questionTableVOS.size(); i++) {
            questionTableVOS.get(i).setAc("1".equals(isAcArray.get(i)));
        }
    }

    @Override
    public OjQuestionPublish publishQuestionAudit(Long questionAuditId) {
        OjQuestionPublish byId = getById(questionAuditId);
        OjQuestionPublish ojQuestionPublish = BeanUtil.copyProperties(ojQuestionAuditService.getById(questionAuditId), OjQuestionPublish.class, "state");
        // 继承之前的数据
        ojQuestionPublish.setSubmitNum(byId == null ? 0 : byId.getSubmitNum());
        ojQuestionPublish.setAcNum(byId == null ? 0 : byId.getAcNum());
        ojQuestionPublish.setPassingRate(byId == null ? new BigDecimal("0") : byId.getPassingRate());
        ojQuestionPublish.setVersion(byId == null ? 0 : byId.getVersion());
        saveOrUpdate(ojQuestionPublish);
        return ojQuestionPublish;
    }

    @Override
    public OjQuestionPublish publishQuestionToRedis(OjQuestionPublish ojQuestionPublish, long questionId) {
        if (ojQuestionPublish == null) {
            ojQuestionPublish = getById(questionId);
        }
        stringRedisTemplate.opsForValue().set(QuestionRedisConstant.QUESTION_CONTENT_PREFIX + questionId, JSONUtil.toJsonStr(ojQuestionPublish));
        return ojQuestionPublish;
    }

    @Override
    public void publishQuestionToEs(OjQuestionPublish ojQuestionPublish, long questionId) {
        if (ojQuestionPublish == null) {
            ojQuestionPublish = getById(questionId);
        }
        // 保存或者更新
        elasticSearchService.saveOrUpdate(ojQuestionPublish);
        lambdaUpdate()
                .eq(OjQuestionPublish::getId, ojQuestionPublish.getId())
                .set(OjQuestionPublish::getState, QuestionPublishState.PUBLISHED)
                .update();
    }

    @Override
    public boolean cancelSubmitQuestionPublish(Long questionId) {
        // 删MySQL
        boolean success = removeById(questionId);
        // 删除所有与之相关的测评记录
        success &= judgeRecordFeignClient.deleteQuestionJudgeRecord(questionId);
        // 删Redis
        success &= BooleanUtil.isTrue(stringRedisTemplate.delete(QuestionRedisConstant.QUESTION_CONTENT_PREFIX + questionId));
        // 删ES
        success &= elasticSearchService.removeQuestionPublish(questionId);
        return success;
    }

    @Override
    public QuestionPublishDetailVO queryQuestionDetailById(Long userId, Long questionId) {
        OjQuestionPublish byId = getById(questionId);
        if (byId == null) {
            throw new ResourceNotFoundException();
        }
        QuestionPublishDetailVO questionPublishDetailVO = BeanUtil.copyProperties(byId, QuestionPublishDetailVO.class);
        questionPublishDetailVO.setIsSelf(questionPublishDetailVO.getUploaderId().equals(userId));
        return questionPublishDetailVO;
    }

    @Override
    public boolean increaseSubmitNum(Long questionId) {
        return ojQuestionPublishMapper.increaseSubmitNum(questionId);
    }

    @Override
    public boolean increaseAcNum(Long questionId) {
        return ojQuestionPublishMapper.increaseAcNum(questionId);
    }

    @Override
    public List<UserDTO> questionRandomAcUser(Long questionId) {
        List<String> userIds = stringRedisTemplate.opsForSet()
                .randomMembers(QuestionRedisConstant.QUESTION_AC_PREFIX + questionId, 10);
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyList();
        }
        return userIds.stream()
                .distinct()
                .map((uId) -> userFeignClient.basicInfoById(Long.parseLong(uId)))
                .collect(Collectors.toList());
    }
}
